/*
 *  Hostroute - create host routes for cellular services based on each
 *  service's Cellular.UsageUrl property.
 *
 *  This file initially created by Google, Inc.
 *  Copyright (C) 2011 The Chromium OS Authors. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <glib.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/assert.h>
#include <connman/dns_client.h>
#include <connman/plugin.h>
#include <connman/log.h>
#include <connman/notifier.h>
#include <connman/service.h>

#define	_DBG_HOSTROUTE(fmt, arg...)	DBG(DBG_HOSTROUTE, fmt, ## arg)

/*
 * Represents a pending DNS request for |hostname|. We will use the result
 * to install a host route through the interface associated with |service|.
 */
struct dns_request {
	struct connman_service *service;
	char *hostname;
	connman_dns_client_request_t handle;  /* opaque to us */
};

/*
 * List of pending asynchronous name resolution requests. We expect the number
 * of pending requests to be small, probably one, because we allow only one
 * pending request per cellular service, and there is typically one active
 * cellular service per device.
 */
static GList *pending_dns_requests = NULL;

/*
 * Destroy a dns_request struct, freeing the resources allocated in
 * submit_dns_request. The request must already have been removed from the
 * |pending_dns_requests| list.
 */
static void destroy_dns_request(struct dns_request *request)
{
	_DBG_HOSTROUTE("request %p", request);
	connman_dns_client_cancel_request(request->handle);
	connman_service_unref(request->service);
	g_free(request->hostname);
	g_free(request);
}

/*
 * Callback that is invoked by c-ares when an asynchronous name resolution
 * request that we have previously initiated is complete.
 */
static void dns_request_cb(void *data,
			   connman_dns_client_status_t status,
			   struct sockaddr *ip_addr)
{
	struct dns_request *request = data;
	char ip_addr_string[INET_ADDRSTRLEN];
	struct sockaddr_in *sin;

	_DBG_HOSTROUTE("request %p: status = %d", request, status);

	pending_dns_requests = g_list_remove(pending_dns_requests, request);

	if (status != CONNMAN_DNS_CLIENT_SUCCESS) {
		connman_error("%s: ares request for '%s' failed: %s", __func__,
			      request->hostname,
			      connman_dns_client_strerror(status));
		destroy_dns_request(request);
		return;
	}

	/*
	 * We only accept IPv4 results because the sticky route code and the
	 * underlying host route code that it uses only work with IPv4 addrs.
	 */
	if (ip_addr->sa_family != AF_INET) {
		connman_error("%s: invalid addr family: %d", __func__,
			      ip_addr->sa_family);
		destroy_dns_request(request);
		return;
	}

	/* Convert sockaddr into a dotted decimal string. */
	sin = (struct sockaddr_in *) ip_addr;
	if (inet_ntop(sin->sin_family, &sin->sin_addr, ip_addr_string,
		      sizeof(ip_addr_string)) == NULL) {
		connman_error("%s: could not convert address to string: %s",
			      __func__, strerror(errno));
		destroy_dns_request(request);
		return;
	}
	_DBG_HOSTROUTE("dns request for '%s' succeeded: %s", request->hostname,
		       ip_addr_string);

	/* Ask the flimflam service core to create and maintain a host route. */
	connman_service_create_sticky_route(request->service, ip_addr_string);

	destroy_dns_request(request);
}

/*
 * Initiate an asynchronous name resolution request for |hostname| in the
 * context of |service|. Returns TRUE on success and FALSE on failure. Success
 * indicates only that the request was initiated, not the success of the
 * request itself.
 */
static gboolean submit_dns_request(struct connman_service *service,
				   const char *hostname)
{
	struct dns_request *request;
	struct connman_device *device;
	int timeout_ms = 0;  /* use default timeout */

	_DBG_HOSTROUTE("service %p: hostname = %s", service, hostname);

	request = g_malloc0(sizeof(struct dns_request));
	request->service = connman_service_ref(service);
	request->hostname = g_strdup(hostname);

	/*
	 * We want to send DNS requests on the interface associated with the
	 * cellular service.
	 */
	device = connman_service_get_device(request->service);

	request->handle = connman_dns_client_submit_request(request->hostname,
							    device,
							    timeout_ms,
							    dns_request_cb,
							    request);
	if (request->handle == NULL) {
		_DBG_HOSTROUTE("could not submit dns request for \"%s\"",
			       request->hostname);
		destroy_dns_request(request);
		return FALSE;
	}

	pending_dns_requests = g_list_append(pending_dns_requests, request);

	return TRUE;
}

/* Cancel any in-progress dns request for |service|. */
static void cancel_dns_request(struct connman_service *service)
{
	GList *node;
	struct dns_request *request;

	_DBG_HOSTROUTE("service %p", service);

	for (node = pending_dns_requests; node != NULL;
	     node = g_list_next(node)) {
		request = node->data;
		if (request->service == service)
			break;
	}

	if (node == NULL)
		return;

	pending_dns_requests = g_list_delete_link(pending_dns_requests, node);
	destroy_dns_request(request);
}

/* Cancel all in-progress dns requests. */
static void cancel_all_dns_requests()
{
	GList *node;
	struct dns_request *request;

	_DBG_HOSTROUTE("");

	while ((node = g_list_first(pending_dns_requests)) != NULL) {
		request = node->data;
		pending_dns_requests = g_list_delete_link(pending_dns_requests,
							  node);
		destroy_dns_request(request);
	}
}

/*
 * Parse the hostname component from a url.
 * Returns a dynamically allocated string in |*hostname_out| that
 * must be g_freed by the caller when no longer needed.
 */
static void hostname_from_url(const char *url, char **hostname_out)
{
	const char *start, *end;
	const char *path, *params, *query, *fragment, *port;
	size_t num_bytes_to_copy;

	/* Ignore protocol prefix. */
	start = strstr(url, "://");
	if (start != NULL)
		start += strlen("://");
	else
		start = url;

	/*
	 * Isolate netloc component of the url.
	 * We search for the earliest delimiter representing the start of a
	 * path, params, query, or fragment component of the url.
	 * See RFC 1808 for details on url syntax.
	 */
	end = url + strlen(url);
	path = strchr(start, '/');
	if (path != NULL)
		end = path;
	params = strchr(start, ';');
	if (params != NULL && params < end)
		end = params;
	query = strchr(start, '?');
	if (query != NULL && query < end)
		end = query;
	fragment = strchr(start, '#');
	if (fragment != NULL && fragment < end)
		end = fragment;

	/* Strip off port if it's part of netloc, leaving hostname. */
	port = strchr(start, ':');
	if (port != NULL && port < end)
		end = port;

	num_bytes_to_copy = end - start;
	*hostname_out = g_strndup(start, num_bytes_to_copy);
}

/*
 * Notification handler that is called by flimflam core when service state
 * changes. We begin the process of installing a sticky host route if the
 * service is cellular, connected, and doesn't already have one. Once a host
 * route is installed, it is managed by the flimflam core for the lifetime of
 * the service.
 */
static void hostroute_service_state_changed(struct connman_service *service)
{
	const char *type;
	const char *state;
	const char *usage_url;
	const char *sticky_route;
	char *hostname;

	_DBG_HOSTROUTE("service %p", service);

	sticky_route = connman_service_get_sticky_route(service);
	if (sticky_route != NULL)
		return;

	type = connman_service_get_type(service);
	if (g_strcmp0(type, "cellular") != 0)
		return;

	state = connman_service_get_state(service);
	if (g_strcmp0(state, "ready") != 0 &&
	    g_strcmp0(state, "online") != 0 &&
	    g_strcmp0(state, "portal") != 0) {
		cancel_dns_request(service);
		return;
	}

	usage_url = connman_service_get_usage_url(service);
	if (usage_url == NULL) {
		connman_error("%s: service %p: no usage url", __func__,
			      service);
		return;
	}
	_DBG_HOSTROUTE("usage_url = %s", usage_url);

	hostname_from_url(usage_url, &hostname);
	_DBG_HOSTROUTE("hostname = %s", hostname);

	cancel_dns_request(service);

	if (!submit_dns_request(service, hostname)) {
		connman_error("%s: service %p: couldn't submit dns request for "
			      "%s", __func__, service, hostname);
		/* fall through for cleanup */
	}
	g_free(hostname);

	/*
	 * Host route installation continues in dns_request_cb when the name
	 * resolution that we've just initiated completes.
	 */
}

/* Define a notifier that will notify us on service state changes. */
static struct connman_notifier hostroute_notifier = {
	.name			= "hostroute",
	.priority		= CONNMAN_NOTIFIER_PRIORITY_DEFAULT,
	.service_state_changed 	= hostroute_service_state_changed
};

/* Initialize the plugin. */
static int hostroute_init(void)
{
	_DBG_HOSTROUTE("");
	if (connman_notifier_register(&hostroute_notifier) < 0) {
		connman_error("%s: Failed to register hostroute notifier",
			      __func__);
		return -1;
	}
	return 0;
}

/* Clean up. */
static void hostroute_exit(void)
{
	_DBG_HOSTROUTE("");
	connman_notifier_unregister(&hostroute_notifier);
	cancel_all_dns_requests();
}

/* Register this plugin with the flimflam plugin system. */
CONNMAN_PLUGIN_DEFINE(hostroute, "Hostroute plugin", VERSION,
		CONNMAN_PLUGIN_PRIORITY_DEFAULT, hostroute_init, hostroute_exit)
